/* SPDX-License-Identifier: GPL-2.0+
 *
 * (C) Copyright 2020
 * Anton Puiu, anton.puiu@email.com
 */

#include "../types/bool.h"
#include "error.h"
#include "so_functions.h"

struct _so_file {
	pid_t pid;
	int fd;
	MODE mode;
	unsigned int size;
	unsigned int current_pos;
	unsigned int available_data;
	unsigned int cursor;
	unsigned char *buffer;
	LAST_OP last_op;
	BOOL error_occured;
};

#ifdef DEBUG
int so_file_get_size(void)
{
	return sizeof(SO_FILE);
}
#endif

static inline SO_FILE *so_file_constructor(const char *mode)
{
	SO_FILE *file;

	file = (SO_FILE *)malloc(sizeof(SO_FILE));

	if (file == NULL)
		return error_allocate_file();

	file->buffer =
		(unsigned char *)malloc(BUF_SIZE * sizeof(unsigned char));

	if (file->buffer == NULL)
		return error_allocate_buffer(file);

	file->mode = mode_get(mode);
	file->current_pos = 0;
	file->available_data = 0;
	file->last_op = UNKNOWN_OP;
	file->error_occured = FALSE;
	file->cursor = 0;
	file->size = 0;

	return file;
}

/* SO_FILE OPEN HELPERS START */
static inline SO_FILE *so_file_read_mode(SO_FILE *file, const char *pathname)
{
#ifdef DEBUG
	printf(READ_MODE_SELECTED);
#endif

	if (so_access(file, pathname, F_OK) == NULL)
		return so_free(file->buffer, file, NULL);

	if (so_access(file, pathname, R_OK) == NULL)
		return so_free(file->buffer, file, NULL);

	file->fd = open(pathname, O_RDONLY);

	if (file->fd == -1)
		return error_open_file(file, file->buffer);

	return file;
}

static inline SO_FILE *so_file_read_update_mode(SO_FILE *file,
						const char *pathname)
{
#ifdef DEBUG
	printf(READ_WRITE_MODE_SELECTED);
#endif

	if (so_access(file, pathname, F_OK) == NULL)
		return so_free(file->buffer, file, NULL);
	if (so_access(file, pathname, R_OK) == NULL)
		return so_free(file->buffer, file, NULL);
	if (so_access(file, pathname, W_OK) == NULL)
		return so_free(file->buffer, file, NULL);

	file->fd = open(pathname, O_RDWR);

	if (file->fd == -1)
		return error_open_file(file, file->buffer);
	else
		return file;
}

static inline SO_FILE *so_file_write_mode(SO_FILE *file, const char *pathname)
{
#ifdef DEBUG
	printf(WRITE_MODE_SELECTED);
#endif

	if (so_access(file, pathname, F_OK) == NULL) {
#ifdef DEBUG
		printf(FILE_NOT_FOUND_CREATE);
#endif

		file->fd = open(pathname, O_WRONLY | O_CREAT, PERMISSIONS);

		if (file->fd == -1)
			return error_create_file(file, file->buffer);
		else
			return file;
	} else if (so_access(file, pathname, W_OK) == NULL) {
		return so_free(file->buffer, file, NULL);
	}

#ifdef DEBUG
	printf(TRUNC_EXISTING_FILE);
#endif

	file->fd = open(pathname, O_WRONLY | O_TRUNC);

	if (file->fd == -1)
		return error_open_file(file, file->buffer);
	else
		return file;
}

static inline SO_FILE *so_file_write_update_mode(SO_FILE *file,
						 const char *pathname)

{
#ifdef DEBUG
	printf(READ_WRITE_MODE_SELECTED);
#endif

	if (so_access(file, pathname, F_OK) == NULL) {
#ifdef DEBUG
		printf(FILE_NOT_FOUND_CREATE);
#endif

		file->fd = open(pathname, O_RDWR | O_CREAT, PERMISSIONS);

		if (file->fd == -1)
			return error_create_file(file, file->buffer);
		else
			return file;
	} else if (so_access(file, pathname, R_OK) == NULL) {
		return so_free(file->buffer, file, NULL);
	} else if (so_access(file, pathname, W_OK) == NULL) {
		return so_free(file->buffer, file, NULL);
	}

#ifdef DEBUG
	printf(TRUNC_EXISTING_FILE);
#endif

	file->fd = open(pathname, O_RDWR | O_TRUNC);

	if (file->fd == -1)
		return error_open_file(file, file->buffer);
	else
		return file;
}

static inline SO_FILE *so_file_append_mode(SO_FILE *file, const char *pathname)
{
#ifdef DEBUG
	printf(APPEND_MODE_SELECTED);
#endif

	if (so_access(file, pathname, F_OK) == NULL) {
#ifdef DEBUG
		printf(FILE_NOT_FOUND_CREATE);
#endif

		file->fd = open(pathname, O_WRONLY | O_APPEND | O_CREAT,
				PERMISSIONS);

		if (file->fd == -1)
			return error_create_file(file, file->buffer);
		else
			return file;
	} else if (so_access(file, pathname, W_OK) == NULL) {
		return so_free(file->buffer, file, NULL);
	}

#ifdef DEBUG
	printf(FILE_OPEN_APPEND);
#endif

	file->fd = open(pathname, O_WRONLY | O_APPEND | O_WRONLY);

	if (file->fd == -1)
		return error_open_file(file, file->buffer);
	else
		return file;
}

static inline SO_FILE *so_file_append_update_mode(SO_FILE *file,
						  const char *pathname)
{
#ifdef DEBUG
	printf(APPEND_READ_MODE_SELECTED);
#endif

	if (so_access(file, pathname, F_OK) == NULL) {
#ifdef DEBUG
		printf(FILE_NOT_FOUND_CREATE);
#endif

		file->fd = open(pathname, O_RDWR | O_APPEND | O_CREAT,
				PERMISSIONS);

		if (file->fd == -1)
			return error_create_file(file, file->buffer);
		else
			return file;
	} else if (so_access(file, pathname, R_OK) == NULL) {
		return so_free(file->buffer, file, NULL);
	} else if (so_access(file, pathname, W_OK) == NULL) {
		return so_free(file->buffer, file, NULL);
	}

#ifdef DEBUG
	printf(FILE_OPEN_APPEND);
#endif

	file->fd = open(pathname, O_RDWR | O_APPEND);

	if (file->fd == -1)
		return error_open_file(file, file->buffer);
	else
		return file;
}

static inline void *so_file_unknown_mode(SO_FILE *file)
{
#ifdef DEBUG
	printf(UNKNOWN_MODE_SELECTED);
#endif

	return so_free(file->buffer, file, NULL);
}
/* SO_FILE OPEN HELPERS FINISH */

/* SO_FILE READ/WRITE FUNCTIONS START */

static inline BOOL so_file_buffer_is_null(SO_FILE *file)
{
	if (file->current_pos == 0 && file->available_data == 0)
		return TRUE;
	else
		return FALSE;
}

static inline int so_read_helper(int fd, void *buffer, size_t count)
{
	int total_readed = 0;
	int bytes = read(fd, buffer, count);

	if (bytes == 0)
		return SO_EOF;
	else if (bytes == -1)
		return 0;

	total_readed += bytes;

	while (total_readed != count) {
		bytes = read(fd, (buffer + total_readed), count - total_readed);

		if (bytes == 0)
			return total_readed;
		else if (bytes == -1)
			return 0;

		total_readed += bytes;
	}

	return total_readed;
}

static inline int so_write_helper(int fd, void *buffer, size_t count)
{
	int total_writed = 0;
	int bytes = write(fd, buffer, count);

	if (bytes == 0)
		return total_writed;
	else if (bytes == -1)
		return 0;

	total_writed += bytes;

	while (total_writed != count) {
		bytes = write(fd, (buffer + total_writed),
			      count - total_writed);

		if (bytes == 0)
			return total_writed;
		else if (bytes == -1)
			return 0;

		total_writed += bytes;
	}

	return total_writed;
}

static inline int so_read_from_file(SO_FILE *file)
{
	int ret_value;
	int bytes_left;
	int bytes_to_read;
	int offset = lseek(file->fd, 0, SEEK_CUR);

	bytes_left = file->size - offset;
	bytes_to_read = bytes_left > BUF_SIZE ? BUF_SIZE : bytes_left;

	ret_value = so_read_helper(file->fd, file->buffer, bytes_to_read);

	if (ret_value == 0) {
		file->error_occured = TRUE;

		return 0;
	}

	file->current_pos = 0;
	file->available_data = bytes_to_read;

	return ret_value;
}

static inline int so_read_from_buffer(SO_FILE *file, char *dest, int count)
{
	int ret_value;
	unsigned int bytes_readed;
	unsigned int bytes_to_read;
	unsigned int offset;

	if (count <= BUF_SIZE) {
		if ((file->current_pos == 0 && file->available_data == 0) ||
		    (file->current_pos == BUF_SIZE &&
		     file->available_data == 0)) {
			bytes_readed = 0;
			ret_value = so_read_from_file(file);

			if (ret_value == 0)
				return 0;

			memcpy(dest, &file->buffer[file->current_pos], count);

			bytes_readed += count;

			file->current_pos += count;
			file->available_data -= count;

			file->error_occured = FALSE;

			return bytes_readed;
		} else if (file->current_pos != 0 &&
			   file->available_data != 0) {
			if (count > file->available_data) {
				bytes_to_read = count - file->available_data;
				offset = file->available_data;
				bytes_readed = 0;

				memcpy(dest, &file->buffer[file->current_pos],
				       file->available_data);

				bytes_readed += file->available_data;

				ret_value = so_read_from_file(file);

				if (ret_value == 0)
					return 0;

				memcpy(dest + offset,
				       &file->buffer[file->current_pos],
				       bytes_to_read);

				bytes_readed += bytes_to_read;

				file->current_pos += bytes_to_read;
				file->available_data -= bytes_to_read;

				file->error_occured = FALSE;

				return bytes_readed;
			} else if (count <= file->available_data) {
				bytes_readed = 0;
				memcpy(dest, &file->buffer[file->current_pos],
				       count);

				bytes_readed += count;

				file->current_pos += count;
				file->available_data -= count;

				file->error_occured = FALSE;

				return bytes_readed;
			}
		}
	} else {
		if ((file->current_pos == 0 && file->available_data == 0) ||
		    (file->current_pos == BUF_SIZE &&
		     file->available_data == 0)) {
			bytes_readed = 0;
			offset = 0;
			bytes_to_read = count;

			while (bytes_to_read > BUF_SIZE) {
				ret_value = so_read_from_file(file);

				if (ret_value == 0)
					return 0;

				memcpy(dest + offset, file->buffer, BUF_SIZE);

				offset += BUF_SIZE;
				bytes_to_read -= BUF_SIZE;
				bytes_readed += BUF_SIZE;
			}

			ret_value = so_read_from_file(file);

			if (ret_value == 0)
				return 0;
			else if (ret_value == SO_EOF)
				return file->size;

			memcpy(dest + offset, file->buffer, bytes_to_read);

			file->current_pos += bytes_to_read;
			file->available_data -= bytes_to_read;
			bytes_readed += bytes_to_read;

			file->error_occured = FALSE;

			return bytes_readed;
		} else if (file->current_pos != 0 &&
			   file->available_data != 0) {
			bytes_readed = 0;
			offset = 0;
			bytes_to_read = count;

			memcpy(dest, &file->buffer[file->current_pos],
			       file->available_data);

			bytes_readed += file->available_data;
			offset += file->available_data;
			bytes_to_read -= file->available_data;

			while (bytes_to_read > BUF_SIZE) {
				ret_value = so_read_from_file(file);

				if (ret_value == 0)
					return 0;

				memcpy(dest + offset, file->buffer, BUF_SIZE);

				offset += BUF_SIZE;
				bytes_to_read -= BUF_SIZE;
				bytes_readed += BUF_SIZE;
			}

			ret_value = so_read_from_file(file);

			if (ret_value == 0)
				return 0;

			memcpy(dest + offset, file->buffer, bytes_to_read);

			file->current_pos += bytes_to_read;
			file->available_data -= bytes_to_read;
			bytes_readed += bytes_to_read;

			file->error_occured = FALSE;

			return bytes_readed;
		}
	}

	return 0;
}

static inline int so_write_to_buffer(SO_FILE *file, const char *src, int count)
{
	int bytes_to_write;
	int bytes_writed;
	int offset;

	if (count <= BUF_SIZE) {
		if (file->current_pos == 0 && file->available_data == 0) {
			// buffer gol, stare initiala.
			memcpy(file->buffer, src, count);

			file->current_pos = count;
			file->available_data = BUF_SIZE - count;

			file->error_occured = FALSE;

			return count;
		} else if (file->current_pos == BUF_SIZE &&
			   file->available_data == 0) {
			// buffer plin, stare finala.
			if (so_fflush(file) == SO_EOF) {
				file->error_occured = TRUE;

				return error_fflush();
			}

			memcpy(file->buffer, src, count);

			file->current_pos = count;
			file->available_data = BUF_SIZE - count;

			file->error_occured = FALSE;

			return count;
		} else if (file->current_pos != 0 &&
			   file->available_data != 0) {
			// regim tranzitoriu.
			if (count > file->available_data) {
				bytes_to_write = count;
				bytes_writed = 0;
				offset = 0;

				memcpy(&file->buffer[file->current_pos], src,
				       file->available_data);

				offset += file->available_data;
				bytes_writed += file->available_data;
				bytes_to_write -= file->available_data;

				file->current_pos += file->available_data;
				file->available_data -= file->available_data;

				if (so_fflush(file) == SO_EOF) {
					file->error_occured = TRUE;

					return error_fflush();
				}

				memcpy(file->buffer, src + offset,
				       bytes_to_write);

				bytes_writed += bytes_to_write;

				file->current_pos += bytes_to_write;
				file->available_data =
					BUF_SIZE - bytes_to_write;

				file->error_occured = FALSE;

				return bytes_writed;
			} else if (count <= file->available_data) {
				memcpy(&file->buffer[file->current_pos], src,
				       count);

				file->current_pos += count;
				file->available_data -= count;

				file->error_occured = FALSE;

				return count;
			}
		}
	} else {
		if (file->current_pos == 0 && file->available_data == 0) {
			// buffer gol, stare initiala.
			bytes_writed = 0;
			offset = 0;
			bytes_to_write = count;

			while (bytes_to_write > BUF_SIZE) {
				memcpy(file->buffer, src + offset, BUF_SIZE);

				offset += BUF_SIZE;
				bytes_writed += BUF_SIZE;
				bytes_to_write -= BUF_SIZE;

				file->current_pos = BUF_SIZE;
				file->available_data = 0;

				if (so_fflush(file) == SO_EOF) {
					file->error_occured = TRUE;

					return error_fflush();
				}
			}

			memcpy(file->buffer, src + offset, bytes_to_write);

			bytes_writed += bytes_to_write;

			file->current_pos += bytes_to_write;
			file->available_data -= bytes_to_write;

			file->error_occured = FALSE;

			return bytes_writed;
		} else if (file->current_pos == BUF_SIZE &&
			   file->available_data == 0) {
			// buffer plin, stare finala.
			if (so_fflush(file) == SO_EOF) {
				file->error_occured = TRUE;

				return error_fflush();
			}

			bytes_writed = 0;
			offset = 0;
			bytes_to_write = count;

			while (bytes_to_write > BUF_SIZE) {
				memcpy(file->buffer, src + offset, BUF_SIZE);

				offset += BUF_SIZE;
				bytes_writed += BUF_SIZE;
				bytes_to_write -= BUF_SIZE;

				file->current_pos = BUF_SIZE;
				file->available_data = 0;

				if (so_fflush(file) == SO_EOF) {
					file->error_occured = TRUE;

					return error_fflush();
				}
			}

			memcpy(file->buffer, src + offset, bytes_to_write);

			bytes_writed += bytes_to_write;

			file->current_pos += bytes_to_write;
			file->available_data -= bytes_to_write;

			file->error_occured = FALSE;

			return bytes_writed;
		} else if (file->current_pos != 0 &&
			   file->available_data != 0) {
			// regim tranzitoriu.
			bytes_writed = 0;
			offset = 0;
			bytes_to_write = count;

			memcpy(&file->buffer[file->current_pos], src,
			       file->available_data);

			bytes_writed += file->available_data;
			offset += file->available_data;
			bytes_to_write -= file->available_data;

			file->current_pos += file->available_data;
			file->available_data -= file->available_data;

			if (so_fflush(file) == SO_EOF) {
				file->error_occured = TRUE;

				return error_fflush();
			}

			while (bytes_to_write > BUF_SIZE) {
				memcpy(file->buffer, src + offset, BUF_SIZE);

				offset += BUF_SIZE;
				bytes_writed += BUF_SIZE;
				bytes_to_write -= BUF_SIZE;

				file->current_pos = BUF_SIZE;
				file->available_data = 0;

				if (so_fflush(file) == SO_EOF) {
					file->error_occured = TRUE;

					return error_fflush();
				}
			}

			memcpy(file->buffer, src + offset, bytes_to_write);

			bytes_writed += bytes_to_write;

			file->current_pos += bytes_to_write;
			file->available_data -= bytes_to_write;

			file->error_occured = FALSE;

			return bytes_writed;
		}
	}

	return 0;
}

static inline void *so_write_to_file(SO_FILE *file, char *buf, int count)
{
	int ret_value;

	if (file->current_pos + count > BUF_SIZE) {
		ret_value = so_fflush(file);

		if (ret_value == SO_EOF) {
			file->error_occured = TRUE;

			return NULL;
		}
	}

	memcpy(&file->buffer[file->current_pos], buf, count);

	file->available_data += count;

	return file;
}
/* SO_FILE READ/WRITE HELPERS FINISH*/

FUNC_DECL_PREFIX SO_FILE *so_fopen(const char *pathname, const char *mode)
{
	SO_FILE *file = so_file_constructor(mode);
	struct stat file_stat;
	void *ret_value;

	switch (file->mode) {
	case READ:
		ret_value = so_file_read_mode(file, pathname);
		break;
	case READ_UPDATE:
		ret_value = so_file_read_update_mode(file, pathname);
		break;
	case WRITE:
		ret_value = so_file_write_mode(file, pathname);
		break;
	case WRITE_UPDATE:
		ret_value = so_file_write_update_mode(file, pathname);
		break;
	case APPEND:
		ret_value = so_file_append_mode(file, pathname);
		break;
	case APPEND_UPDATE:
		ret_value = so_file_append_update_mode(file, pathname);
		break;
	default:
		return so_file_unknown_mode(file);
	}

	if (ret_value == NULL)
		return ret_value;

	if (fstat(file->fd, &file_stat) == -1)
		return error_file_stat(file, file->buffer);

	file->size = (unsigned int)file_stat.st_size;

	return file;
}

FUNC_DECL_PREFIX int so_fclose(SO_FILE *stream)
{
	int fd = stream->fd;
	int ret_value = 0;

	if (stream->current_pos != 0 && stream->last_op == WRITE_OP)
		if (so_fflush(stream) == SO_EOF)
			ret_value = SO_EOF;

	so_free(stream->buffer, stream, NULL);

	if (close(fd) == -1)
		return SO_EOF;

	return ret_value;
}

FUNC_DECL_PREFIX int so_fileno(SO_FILE *stream)
{
	return stream->fd;
}

FUNC_DECL_PREFIX int so_fflush(SO_FILE *stream)
{
	int ret_value = so_write_helper(stream->fd, stream->buffer,
					stream->current_pos);

	if (ret_value == 0)
		return SO_EOF;

	stream->current_pos = 0;
	stream->available_data = 0;

	stream->last_op = UNKNOWN_OP;

	return 0;
}

FUNC_DECL_PREFIX int so_fseek(SO_FILE *stream, long offset, int whence)
{
	int i;

	if (stream->last_op == READ_OP) {
		for (i = 0; i < stream->available_data; i++)
			stream->buffer[i] = 0;

		stream->current_pos = 0;
		stream->available_data = 0;
	} else if (stream->last_op == WRITE_OP) {
		if (so_fflush(stream) == SO_EOF) {
			stream->error_occured = TRUE;

			return SO_EOF;
		}
	}

	stream->cursor = lseek(stream->fd, offset, whence);

	return 0;
}

FUNC_DECL_PREFIX long so_ftell(SO_FILE *stream)
{
	return stream->cursor;
}

FUNC_DECL_PREFIX
size_t so_fread(void *ptr, size_t size, size_t nmemb, SO_FILE *stream)
{
	int count = size * nmemb;
	int ret_value = so_read_from_buffer(stream, ptr, count);

	stream->last_op = READ_OP;
	stream->cursor += count;

	return ret_value == 0 ? 0 : ret_value / size;
}

FUNC_DECL_PREFIX
size_t so_fwrite(const void *ptr, size_t size, size_t nmemb, SO_FILE *stream)
{
	int count = size * nmemb;
	int ret_value = so_write_to_buffer(stream, ptr, count);

	stream->last_op = WRITE_OP;
	stream->cursor += count;

	return ret_value == 0 ? 0 : ret_value / size;
}

FUNC_DECL_PREFIX int so_fgetc(SO_FILE *stream)
{
	char c;

	stream->last_op = READ_OP;
	stream->cursor++;

	return so_read_from_buffer(stream, &c, 1) == 0 ? SO_EOF : (int)c;
}

FUNC_DECL_PREFIX int so_fputc(int c, SO_FILE *stream)
{
	char ch = (char)c;

	stream->last_op = WRITE_OP;
	stream->cursor++;

	return so_write_to_buffer(stream, &ch, 1) == 0 ? SO_EOF : c;
}

FUNC_DECL_PREFIX int so_feof(SO_FILE *stream)
{
	return stream->size < stream->cursor ? TRUE : FALSE;
}

FUNC_DECL_PREFIX int so_ferror(SO_FILE *stream)
{
	return stream->error_occured;
}

FUNC_DECL_PREFIX SO_FILE *so_popen(const char *command, const char *type)
{
	return NULL;
}

FUNC_DECL_PREFIX int so_pclose(SO_FILE *stream)
{
	return 0;
}
